package epss

import (
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"
)

/////////////////////////////////////////////////////////////
//
// EPSS API DOCS:
//
// https://api.first.org/epss
// https://www.first.org/epss/api
//
// EPSS OFFLINE DATA:
//
// ALL EPSS scores for all CVEs for a particular date (yyyy-mm-dd).
// For this request, simply request the full csv directly as
// https://epss.cyentia.com/epss_scores-YYYY-MM-DD.csv.gz
//
/////////////////////////////////////////////////////////////

type ReplyMetadata struct {
	Status     string `json:"status"`
	StatusCode int    `json:"status-code"`
	Version    string `json:"version"`
	Access     string `json:"access"`
	Total      uint64 `json:"total"`
	Offset     uint64 `json:"offset"`
	Limit      uint64 `json:"limit"`
}

type ReplyType interface {
	Metadata() *ReplyMetadata
}

type Reply struct {
	*ReplyMetadata
}

func (ref *Reply) Metadata() *ReplyMetadata {
	return ref.ReplyMetadata
}

const (
	APITimeout = 20
	PageSize   = uint64(100)
)

const (
	OutJSON = "application/json"
	OutYAML = "application/yaml"
	OutXML  = "application/xml"
	OutCSV  = "application/csv"
)

var (
	ErrInvalidParams    = errors.New("invalid params")
	ErrInvalidDateParam = errors.New("invalid date param")
	ErrInvalidCVEParam  = errors.New("invalid CVE param")
	ErrNotFound         = errors.New("not found")
	ErrNotAuthorized    = errors.New("not authorized")
	ErrTooManyCVEs      = errors.New("too many CVEs")
)

type OrderType string

const (
	NoOrder             = ""
	ScoreDescOrder      = "ot.score.desc"
	ScoreAscOrder       = "ot.score.asc"
	PercentileDescOrder = "ot.percentile.desc"
	PercentileAscOrder  = "ot.percentile.asc"
)

type APIScoreData struct {
	Date       string `json:"date"`
	EPSS       string `json:"epss"`
	Percentile string `json:"percentile"`
}

type APIScore struct {
	APIScoreData
	CVE string `json:"cve"`
}

type APIScoreWithHistory struct {
	APIScore
	TimeSeries []APIScoreData `json:"time-series,omitempty"`
}

type APIResult struct {
	Reply
	Data []*APIScore `json:"data"`
}

type APIResultWithHistory struct {
	Reply
	Data []*APIScoreWithHistory `json:"data"`
}

type Result struct {
	Reply
	Data []*Score `json:"data"`
}

type ResultWithHistory struct {
	Reply
	Data []*ScoreWithHistory `json:"data"`
}

type ScoreData struct {
	Date       time.Time `json:"date"`
	EPSS       float64   `json:"epss"`
	Percentile float64   `json:"percentile"`
}

type Score struct {
	ScoreData
	CVE string `json:"cve"`
}

type ScoreWithHistory struct {
	Score
	History []ScoreData `json:"history,omitempty"`
}

func (ref *ScoreData) MarshalJSON() ([]byte, error) {
	type ScoreDataAlias ScoreData
	return json.Marshal(&struct {
		*ScoreDataAlias
		Date string `json:"date"`
	}{
		ScoreDataAlias: (*ScoreDataAlias)(ref),
		Date:           ref.Date.Format(time.DateOnly),
	})
}

func (ref *Score) MarshalJSON() ([]byte, error) {
	//embedding gotcha workaround
	return json.Marshal(&struct {
		EPSS       float64 `json:"epss"`
		Percentile float64 `json:"percentile"`
		Date       string  `json:"date"`
		CVE        string  `json:"cve"`
	}{
		EPSS:       ref.EPSS,
		Percentile: ref.Percentile,
		Date:       ref.Date.Format(time.DateOnly),
		CVE:        ref.CVE,
	})
}

func (ref *ScoreWithHistory) MarshalJSON() ([]byte, error) {
	//embedding gotcha workaround
	return json.Marshal(&struct {
		EPSS       float64     `json:"epss"`
		Percentile float64     `json:"percentile"`
		Date       string      `json:"date"`
		CVE        string      `json:"cve"`
		History    []ScoreData `json:"history,omitempty"`
	}{
		EPSS:       ref.EPSS,
		Percentile: ref.Percentile,
		Date:       ref.Date.Format(time.DateOnly),
		CVE:        ref.CVE,
		History:    ref.History,
	})
}

func NewResult(input *APIResult) (*Result, error) {
	if input == nil {
		return nil, nil
	}

	output := Result{
		Reply: Reply{
			ReplyMetadata: input.ReplyMetadata,
		},
	}

	if len(input.Data) == 0 {
		return &output, nil
	}

	output.Data = make([]*Score, 0, len(input.Data))
	for _, rawScore := range input.Data {
		score := Score{
			CVE: rawScore.CVE,
		}

		var err error
		score.Date, err = DateFromString(rawScore.Date)
		if err != nil {
			return nil, err
		}

		score.EPSS, err = strconv.ParseFloat(rawScore.EPSS, 64)
		if err != nil {
			return nil, err
		}

		score.Percentile, err = strconv.ParseFloat(rawScore.Percentile, 64)
		if err != nil {
			return nil, err
		}

		output.Data = append(output.Data, &score)
	}

	return &output, nil
}

func NewResultWithHistory(input *APIResultWithHistory) (*ResultWithHistory, error) {
	if input == nil {
		return nil, nil
	}

	output := ResultWithHistory{
		Reply: Reply{
			ReplyMetadata: input.ReplyMetadata,
		},
	}

	if len(input.Data) == 0 {
		return &output, nil
	}

	output.Data = make([]*ScoreWithHistory, 0, len(input.Data))
	for _, rawScore := range input.Data {
		score := ScoreWithHistory{
			Score: Score{
				CVE: rawScore.CVE,
			},
		}

		var err error
		score.Date, err = DateFromString(rawScore.Date)
		if err != nil {
			return nil, err
		}

		score.EPSS, err = strconv.ParseFloat(rawScore.EPSS, 64)
		if err != nil {
			return nil, err
		}

		score.Percentile, err = strconv.ParseFloat(rawScore.Percentile, 64)
		if err != nil {
			return nil, err
		}

		if len(rawScore.TimeSeries) == 0 {
			continue
		}

		score.History = make([]ScoreData, 0, len(rawScore.TimeSeries))
		for _, rawData := range rawScore.TimeSeries {
			var scoreData ScoreData
			scoreData.Date, err = DateFromString(rawData.Date)
			if err != nil {
				return nil, err
			}

			scoreData.EPSS, err = strconv.ParseFloat(rawData.EPSS, 64)
			if err != nil {
				return nil, err
			}

			scoreData.Percentile, err = strconv.ParseFloat(rawData.Percentile, 64)
			if err != nil {
				return nil, err
			}

			score.History = append(score.History, scoreData)
		}

		output.Data = append(output.Data, &score)
	}

	return &output, nil
}

func Date(year int, month time.Month, day int) time.Time {
	return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
}

func DateFromString(input string) (time.Time, error) {
	date, err := time.Parse(time.DateOnly, input)
	if err != nil {
		return time.Time{}, err
	}

	return date, nil
}

func DateFromStringOrNow(input string) time.Time {
	date, err := DateFromString(input)
	if err == nil {
		return date
	}

	return time.Now()
}

func DateToString(input time.Time) string {
	return input.Format(time.DateOnly)
}

func IsValidDate(input time.Time) bool {
	if input.Before(EarliestDate) ||
		input.After(time.Now().UTC()) {
		return false
	}

	return true
}

var EarliestDate = Date(2021, 4, 14)

func IsValidOutput(input string) bool {
	switch input {
	case OutJSON, OutYAML, OutXML, OutCSV:
		return true
	}

	return false
}

func IsValidCveID(input string) error {
	parts := strings.Split(input, "-")
	if len(parts) != 3 {
		return ErrInvalidCVEParam
	}

	if strings.ToUpper(parts[0]) != "CVE" {
		return ErrInvalidCVEParam
	}

	if len(parts[1]) != 4 {
		return ErrInvalidCVEParam
	}

	if len(parts[2]) < 4 {
		return ErrInvalidCVEParam
	}

	yr, err := strconv.Atoi(parts[1])
	if err != nil {
		return err
	}

	if yr < 1999 || yr > time.Now().Year() {
		return ErrInvalidCVEParam
	}

	sn, err := strconv.Atoi(parts[2])
	if err != nil {
		return err
	}

	if sn < 1 {
		return ErrInvalidCVEParam
	}

	return nil
}

func IsValidCveList(input []string) error {
	for idx, cve := range input {
		if err := IsValidCveID(cve); err != nil {
			return fmt.Errorf("invalid CVE ID: index=%d cve='%s' (%w)", idx, cve, err)
		}
	}

	return nil
}

var (
	_ ReplyType = (*APIResult)(nil)
	_ ReplyType = (*APIResultWithHistory)(nil)
	_ ReplyType = (*Result)(nil)
	_ ReplyType = (*ResultWithHistory)(nil)
)
